"""Manage y-stream composes."""
# pylint: disable=broad-exception-raised,broad-exception-caught
import argparse
from importlib import resources
import json
import logging
import os
import pathlib
import re
import subprocess
import typing

from cki_lib import logger
from cki_lib import misc
from cki_lib import session
from cki_lib import yaml
from cki_lib.config_tree import process_config_tree
import requests
import requests_gssapi

LOGGER: logging.Logger = logger.get_logger(__name__)
SESSION = session.get_session(__name__, raise_for_status=True)


def get_distro_name(distro_name_pattern: str, tags: list[str]) -> str:
    """Query Beaker to get the latest distro."""
    cmd = [
        'bkr', 'distros-list',
        '--limit', '1',
        '--format', 'json',
        '--name', distro_name_pattern,
    ] + [f'--tag={t}' for t in misc.flattened(tags)]
    LOGGER.debug('Running: %s', cmd)
    distro_data = json.loads(subprocess.run(cmd, capture_output=True, check=True).stdout)
    if result := misc.get_nested_key(distro_data, '0/distro_name'):
        return typing.cast(str, result)
    raise Exception(f'Failed to get distro for {distro_name_pattern}')


def parse_tree(tree_path: pathlib.Path) -> dict[str, str]:
    """Update the compose in a kpet-db tree."""
    data = tree_path.read_text(encoding='utf8')
    return {
        name: match[1] for name in ('distro_name', 'buildroot_name')
        if (match := re.search(f'^{{% set {name} = "(.*)" %}}$', data, re.MULTILINE))
    }


def update_tree(tree_path: pathlib.Path, fields: dict[str, str]) -> None:
    """Update the compose in a kpet-db tree."""
    data = tree_path.read_text(encoding='utf8')
    for name, value in fields.items():
        data = re.sub(f'.*set {name}.*', f'{{% set {name} = "{value}" %}}', data, re.MULTILINE)
    tree_path.write_text(data, encoding='utf8')


class Composes:
    """Manage y-stream composes."""

    def __init__(self, kpet_db, config):
        """Manage y-stream composes."""
        self.kpet_db = kpet_db
        self.config = process_config_tree(config)
        self.auth = requests_gssapi.HTTPSPNEGOAuth(mutual_authentication=requests_gssapi.OPTIONAL)

    def cts_request(self, method: str, url: str, **kwargs: typing.Any) -> requests.Response:
        """Send an API request for CTS."""
        return SESSION.request(method, f'{os.environ["CTS_URL"]}/api/1/{url}',
                               auth=self.auth if method in {'PATCH', 'POST'} else None,
                               **kwargs)

    def get_buildroot_name(self, distro_name: str) -> str:
        """Query CTS to find the buildroot compose of given compose."""
        compose_info = self.cts_request('GET', f'composes/{distro_name}').json()
        if result := next((c for c in compose_info['children'] if c.startswith('BUILDROOT')), None):
            return typing.cast(str, result)
        raise Exception(f'Failed to get buildroot for {distro_name}')

    def get_tagged_composes(self, tag: str) -> set[str]:
        """Query CTS to find all composes with a certain tag."""
        return {compose_id for i in self.cts_request('GET', f'composes/?tag={tag}').json()['items']
                if (compose_id := misc.get_nested_key(i, 'compose_info/payload/compose/id'))}

    def update(self) -> int:
        """Update the composes in kpet-db."""
        exit_code = 0
        for tree_name, config in self.config.items():
            with logger.logging_env(config):
                LOGGER.info('Processing %s', tree_name)
                try:
                    tree_path = pathlib.Path(f'{self.kpet_db}/trees/{tree_name}.j2')
                    fields = parse_tree(tree_path)
                    fields['distro_name'] = get_distro_name(
                        config['distro_name'], config.get('tags', []))
                    if 'buildroot_name' in fields:
                        fields['buildroot_name'] = self.get_buildroot_name(fields['distro_name'])
                    LOGGER.info('Found %s', fields)
                    update_tree(tree_path, fields)
                except Exception:
                    LOGGER.exception('Failed to process tree %s', tree_name)
                    exit_code = 1
        return exit_code

    def tag(self, tag: str, user_data: str) -> int:
        """Tag the current y-stream composes in CTS."""
        used_distros = {
            parse_tree(pathlib.Path(f'{self.kpet_db}/trees/{tree_name}.j2'))['distro_name']
            for tree_name in self.config
        }
        tagged_distros = self.get_tagged_composes(tag)
        for superfluous in tagged_distros - used_distros:
            if misc.is_production():
                LOGGER.info('Removing %s from %s', tag, superfluous)
                self.cts_request('PATCH', f'composes/{superfluous}', json={
                    'action': 'untag', 'tag': tag, 'user_data': user_data,
                })
            else:
                LOGGER.info('Production mode would remove %s from %s', tag, superfluous)
        for missing in used_distros - tagged_distros:
            if misc.is_production():
                LOGGER.info('Adding %s to %s', tag, missing)
                self.cts_request('PATCH', f'composes/{missing}', json={
                    'action': 'tag', 'tag': tag, 'user_data': user_data,
                })
            else:
                LOGGER.info('Production mode would add %s to %s', tag, missing)
        return 0


def main(argv: list[str] | None = None) -> int:
    """Manage y-stream composes."""
    parser = argparse.ArgumentParser(description='Manage y-stream trees in kpet-db.')
    parser.add_argument('--config', default=os.environ.get('YSTREAM_COMPOSES_CONFIG'),
                        help='YAML y-stream compose configuration file to use')
    parser.add_argument('--config-path', default=os.environ.get('YSTREAM_COMPOSES_CONFIG_PATH'),
                        help='path to YAML y-stream compose configuration file')
    parser.add_argument('--kpet-db', default='.', help='path to kpet-db')
    subparsers = parser.add_subparsers(dest='action', required=True)

    _ = subparsers.add_parser('update', help='update compose selection')

    parser_tag = subparsers.add_parser('tag', help='synchronize tag in CTS')
    parser_tag.add_argument('tag', help='name of tag to synchronize')
    parser_tag.add_argument('--user-data', default='updating tags to match kpet-db',
                            help='description for compose change history')

    _ = subparsers.add_parser('validate', help='validate the configuration')

    args = parser.parse_args(argv)

    schema_path = resources.files(__package__) / 'schema.yml'
    config = yaml.load(contents=args.config, file_path=args.config_path,
                       schema_path=schema_path,
                       process_config_tree=True, resolve_references=True)
    composes = Composes(args.kpet_db, config)

    match args.action:
        case 'update':
            exit_code = composes.update()
        case 'tag':
            exit_code = composes.tag(args.tag, args.user_data)
        case 'validate':
            exit_code = 0
    return exit_code
